import {
  Component,
  Injector,
  Input,
  Output,
  EventEmitter,
  AfterViewInit,
  OnInit,
  OnDestroy,
  ViewChild,
  ComponentRef,
  ViewContainerRef,
  ComponentFactoryResolver,
  ElementRef,
  Type,
  TemplateRef
} from '@angular/core';
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { Observable, Subject } from 'rxjs';

import { PopupRef, PopupRes, OnClickCallback } from './popup.interface';

@Component({
  selector: 'app-popup',
  templateUrl: './popup.html'
})
export class PopupComponent<T = any, R = any> extends PopupRef<T, R> implements AfterViewInit, OnInit, OnDestroy {

  private unsubscribe$ = new Subject<void>();

  @Output() popupOpen = new EventEmitter<void>();

  @Output() popupClose = new EventEmitter<R>();

  get afterOpen(): Observable<void> {
    return this.popupOpen.asObservable();
  }

  get afterClose(): Observable<R> {
    return this.popupClose.asObservable();
  }

  @Output() visibleChange = new EventEmitter<boolean>();

  @Input() title: string;

  @Input() content: string | Type<T>;

  @Input() componentParams: object;

  @Input() width = 256;

  private contentComponentRef: ComponentRef<T>;

  private container: HTMLElement | OverlayRef;

  @ViewChild('bodyContainer', { read: ViewContainerRef }) bodyContainer: ViewContainerRef;

  @Input() visible = false;

  @Input() @Output() onSubmit: EventEmitter<T> | OnClickCallback<T> = new EventEmitter<T>();

  @Input() @Output() onCancel: EventEmitter<T> | OnClickCallback<T> = new EventEmitter<T>();

  submitLoading = false;

  cancelLoading = false;

  @Input() nzGetContainer: HTMLElement | OverlayRef | (() => HTMLElement | OverlayRef) = () => this.overlay.create();

  constructor(
    private overlay: Overlay,
    private cfr: ComponentFactoryResolver,
    private viewContainer: ViewContainerRef,
    private elementRef: ElementRef
  ) {
    super();
  }

  ngOnInit(): void {
    this.container = typeof this.nzGetContainer === 'function' ? this.nzGetContainer() : this.nzGetContainer;
    if (this.container instanceof HTMLElement) {
      this.container.appendChild(this.elementRef.nativeElement);
    } else if (this.container instanceof OverlayRef) {
      this.container.overlayElement.appendChild(this.elementRef.nativeElement);
    }
  }

  ngAfterViewInit(): void {
    if (this.isComponent(this.content)) {
      this.createDynamicComponent(this.content as Type<T>);
    }

    if (this.contentComponentRef) {
      this.bodyContainer.insert(this.contentComponentRef.hostView);
    }
  }

  ngOnDestroy(): void {
    if (this.container instanceof OverlayRef) {
      this.container.dispose();
    }
    this.unsubscribe$.next();
    this.unsubscribe$.complete();
  }

  open(): void {
    this.changeVisibleFromInside(true);
  }

  close(result?: R): void {
    this.changeVisibleFromInside(false, result);
  }

  destroy(result?: R): void {
    this.close(result);
  }

  triggerSubmit(): void {
    const contentComponent = (this.getContentComponent() as PopupRes);
    try {
      this.submitLoading = true;
      contentComponent.submit().subscribe(res => {
        this.onClickSubmitCancel('submit', res);
        this.submitLoading = false;
      }, err => {
        this.submitLoading = false;
      });
    } catch (e) {
      this.onClickSubmitCancel('submit');
    }
  }

  triggerCancel(): void {
    const contentComponent = (this.getContentComponent() as PopupRes);
    try {
      this.cancelLoading = true;
      contentComponent.cancel().subscribe(res => {
        this.onClickSubmitCancel('cancel', res);
        this.cancelLoading = false;
      }, err => {
        this.cancelLoading = false;
      });
    } catch (e) {
      this.onClickSubmitCancel('cancel');
    }
  }

  getInstance(): PopupComponent {
    return this;
  }

  getContentComponent(): T | any {
    return this.contentComponentRef && this.contentComponentRef.instance;
  }

  isComponent(value: {}): boolean {
    return value instanceof Type;
  }

  isTemplateRef(value: {}): boolean {
    return value instanceof TemplateRef;
  }

  isNonEmptyString(value: {}): boolean {
    return typeof value === 'string' && value !== '';
  }

  private createDynamicComponent(component: Type<T>): void {
    const factory = this.cfr.resolveComponentFactory(component);
    const childInjector = Injector.create({
      providers: [{ provide: PopupRef, useValue: this }],
      parent: this.viewContainer.parentInjector
    });
    this.contentComponentRef = factory.create(childInjector);
    if (this.componentParams) {
      Object.assign(this.contentComponentRef.instance, this.componentParams);
    }
    this.contentComponentRef.changeDetectorRef.detectChanges();
  }

  private changeVisibleFromInside(visible: boolean, closeResult?: R): Promise<void> {
    if (this.visible !== visible) {
      this.visible = visible;
      this.visibleChange.emit(visible);
      return this.handleVisibleStateChange(visible, true, closeResult);
    }
    return Promise.resolve();
  }

  private handleVisibleStateChange(visible: boolean, animation: boolean = true, closeResult?: R): Promise<void> {
    return Promise.resolve()
      .then(res => {
        if (visible) {
          this.popupOpen.emit();
        } else {
          this.popupClose.emit(closeResult);
        }
      });
  }

  private onClickSubmitCancel(type: 'submit' | 'cancel', res?: any): void {
    const trigger = { 'submit': this.onSubmit, 'cancel': this.onCancel }[type];
    const loadingKey = { 'submit': 'submitLoading', 'cancel': 'cancelLoading' }[type];
    if (typeof trigger === 'function') {
      const result = trigger(res);
      const caseClose = (doClose: boolean | void | {}) => (doClose !== false) && this.close(doClose as R); // Users can return "false" to prevent closing by default
      if (isObservable(result)) {
        this[loadingKey] = true;
        const handleThen = (doClose) => {
          this[loadingKey] = false;
          caseClose(doClose);
        };
        (result as Observable<void>).subscribe(handleThen, handleThen);
      } else {
        caseClose(result);
      }
    }
  }

}

function isObservable(obj: {} | void): boolean {
  return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof (obj as Observable<{}>).subscribe === 'function';
}
